API Response Standard
Overview
This document defines the standard API response format for all ATOM SaaS API routes. Consistent response formats improve developer experience, simplify error handling, and ensure proper monitoring.
Standard Response Format
Success Response
{
"data": T, // Response data
"timestamp": string // ISO 8601 timestamp
}Error Response
{
"error": string, // Error message
"code"?: string, // Error code for programmatic handling
"details"?: unknown, // Additional error details
"timestamp": string // ISO 8601 timestamp
}Implementation
Using Helpers (Recommended)
All API routes MUST use the helpers from @/lib/api/api-response:
import { sendApiError, sendApiSuccess, withApiHandler, withTenantContext, Errors } from '@/lib/api/api-response'
// ✅ GOOD: Using helpers
export async function GET(request: Request) {
return withApiHandler(async () => {
return withTenantContext(async ({ id: tenantId }) => {
const result = await getData(tenantId)
return sendApiSuccess(result)
}, request)
})
}
// ❌ BAD: Manual NextResponse.json
export async function GET(request: Request) {
try {
const result = await getData()
return NextResponse.json(result)
} catch (error) {
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}Error Helpers
Use the Errors object for common errors:
import { Errors } from '@/lib/api/api-response'
// Quick error creation
throw Errors.unauthorized('You must log in')
throw Errors.forbidden('Access denied')
throw Errors.notFound('Agent')
throw Errors.badRequest('Invalid input')
throw Errors.conflict('Resource already exists')
throw Errors.rateLimited()
throw Errors.internal('Something went wrong')
throw Errors.validation({ field: 'error' })
throw Errors.paymentRequired('Upgrade required')Route Handler Wrapper
Use withApiHandler for automatic error handling:
export async function POST(request: Request) {
return withApiHandler(async () => {
const body = await request.json()
// Errors automatically caught and formatted
const result = await processData(body)
return sendApiSuccess(result)
})
}Tenant Context Wrapper
Use withTenantContext for guaranteed tenant isolation:
export async function GET(request: Request) {
return withApiHandler(async () => {
return withTenantContext(async ({ id: tenantId }) => {
// Guaranteed to have tenant context here
const agents = await db.query(
'SELECT * FROM agents WHERE tenant_id = $1',
[tenantId]
)
return sendApiSuccess(agents.rows)
}, request)
})
}Rate Limiting
Use withRateLimit for rate-limited routes:
export async function POST(request: Request) {
return withApiHandler(async () => {
return withTenantContext(async ({ id: tenantId }) => {
return withRateLimit(async () => {
const result = await expensiveOperation()
return sendApiSuccess(result)
}, tenantId, redis)
}, request)
})
}Migration Guide
Before (Non-Standard)
export async function GET(req: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized', code: 'UNAUTHORIZED' },
{ status: 401 }
)
}
const tenant = await getTenantFromRequest(req)
if (!tenant) {
return NextResponse.json(
{ error: 'Tenant not found', code: 'TENANT_NOT_FOUND' },
{ status: 404 }
)
}
const result = await getData(tenant.id)
return NextResponse.json(result)
} catch (error) {
console.error('Failed:', error)
return NextResponse.json(
{ error: 'Internal server error', code: 'INTERNAL_ERROR' },
{ status: 500 }
)
}
}After (Standard)
export async function GET(request: Request) {
return withApiHandler(async () => {
return withTenantContext(async ({ id: tenantId }) => {
const result = await getData(tenantId)
return sendApiSuccess(result)
}, request)
})
}Common Error Codes
| Code | Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Authentication required |
FORBIDDEN | 403 | Access denied |
NOT_FOUND | 404 | Resource not found |
BAD_REQUEST | 400 | Invalid request |
VALIDATION_ERROR | 400 | Request validation failed |
CONFLICT | 409 | Resource conflict |
RATE_LIMITED | 429 | Rate limit exceeded |
INTERNAL_ERROR | 500 | Server error |
TENANT_NOT_FOUND | 404 | Tenant not found |
TENANT_CONTEXT_ERROR | 500 | Failed to resolve tenant |
PAYMENT_REQUIRED | 402 | Payment required |
LIMIT_EXCEEDED | 403 | Tier limit exceeded |
Status Codes
Success
200 OK- Successful request201 Created- Resource created204 No Content- Successful request with no response body
Client Errors
400 Bad Request- Invalid request401 Unauthorized- Authentication required402 Payment Required- Payment required403 Forbidden- Access denied404 Not Found- Resource not found409 Conflict- Resource conflict429 Too Many Requests- Rate limit exceeded
Server Errors
500 Internal Server Error- Server error503 Service Unavailable- Service unavailable
Best Practices
1. Always Use Helpers
✅ Use sendApiError, sendApiSuccess, withApiHandler
❌ Don't manually create NextResponse.json
2. Include Error Codes
✅ Use descriptive error codes like AGENT_NOT_FOUND
❌ Don't use generic codes like ERROR
3. Provide Context
✅ Include relevant details in error responses
❌ Don't expose sensitive information in production
4. Use Timestamps
✅ Always include ISO 8601 timestamps
❌ Don't omit timestamps from responses
5. Handle Errors Gracefully
✅ Use try-catch or withApiHandler wrapper
❌ Don't let errors propagate unhandled
Testing
Test Success Response
const response = await fetch('/api/agents')
const data = await response.json()
expect(data).toHaveProperty('data')
expect(data).toHaveProperty('timestamp')
expect(response.status).toBe(200)Test Error Response
const response = await fetch('/api/agents/invalid')
const data = await response.json()
expect(data).toHaveProperty('error')
expect(data).toHaveProperty('code')
expect(data).toHaveProperty('timestamp')
expect(response.status).toBe(404)Monitoring
All API responses should be monitored for:
- Error rates by code
- Response times
- Rate limit violations
- Tenant context resolution failures
Enforcement
Linter Rule
A custom ESLint rule should enforce:
- Use of
sendApiErrorfor error responses - Use of
sendApiSuccessfor success responses - Use of
withApiHandlerwrapper - Inclusion of timestamps
Pre-Commit Hook
A pre-commit hook should check:
- No direct
NextResponse.jsoncalls with error status codes - All API routes use standard helpers
Examples
Complete Example
import { sendApiError, sendApiSuccess, withApiHandler, withTenantContext, Errors } from '@/lib/api/api-response'
export async function GET(request: Request) {
return withApiHandler(async () => {
return withTenantContext(async ({ id: tenantId }) => {
const agents = await db.query(
'SELECT * FROM agents WHERE tenant_id = $1',
[tenantId]
)
return sendApiSuccess(agents.rows)
}, request)
})
}
export async function POST(request: Request) {
return withApiHandler(async () => {
return withTenantContext(async ({ id: tenantId }) => {
const body = await request.json()
if (!body.name) {
throw Errors.badRequest('Name is required')
}
const agent = await createAgent(tenantId, body)
return sendApiSuccess(agent, 201)
}, request)
})
}References
- Implementation:
src/lib/api/api-response.ts - Examples:
src/app/api/agents/[id]/run/route.ts(good example) - Migration Guide: See above
Changelog
- 2026-02-08: Initial standard created
- 2026-02-08: Migration guide added
- 2026-02-08: Helper utilities documented